import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.StringReader;
// -------------------------------------------------------------------------
/**
 *  This filemanager uses the memory manager to write encoded sequences to the
 *  binary file. It also allows for reading from the file for the printing
 *  of the hash table.
 *
 *  @author Erik
 *  @version Apr 10, 2013
 */
public class FileManager {

	private File memoryFile; //The file we are writing to
	private RandomAccessFile fileOutput; //the reader/writer of the file
	private MemoryManager memMGR = new MemoryManager(); //a new memory manager for the file

	// ----------------------------------------------------------
	/**
	 * Create a new FileManager object. Creates a new file and reader/writer for that file
	 * @throws IOException
	 */
	public FileManager(File memoryFile) throws IOException{
		this.memoryFile = memoryFile;
		fileOutput = new RandomAccessFile(memoryFile, "rw");
		fileOutput.setLength(0);
	}

	// ----------------------------------------------------------
	/**
	 * Given a sequence handle, this reads the sequence from the binary file.
	 * @param memHandle read either a sequence or a sequence ID handle
	 * @return the sequence string
	 * @throws IOException
	 */
	public String readHandle(MemBlock memHandle) throws IOException{
		byte[] seqByteArray = new byte[(int)Math.ceil(memHandle.getLength()/4.)];
		fileOutput.seek(memHandle.getOffset());
		fileOutput.read(seqByteArray);
		return bytesToString(seqByteArray, memHandle.getLength());
	}

	// ----------------------------------------------------------
	/**
	 * This method inserts a new sequence into the memory manager and writes it
	 * to the file. It also returns an array of memory handles. One for the
	 * sequence ID, and one for the sequence itself.
	 * @param sequence
	 * @param seqID
	 * @return a sequence handle for the inserted sequence
	 * @throws IOException
	 */
	public MemBlock[] insertSequence(String sequence, String seqID) throws IOException{
		byte[] seqByteArray = stringToBytes(sequence,sequence.length());
		byte[] idByteArray = stringToBytes(seqID, seqID.length());
		MemBlock blockForID = memMGR.getFreeSpace((int)Math.ceil(seqID.length()/4.));
		MemBlock blockForSequence = memMGR.getFreeSpace((int)Math.ceil(sequence.length()/4.));
		fileOutput.seek(blockForID.getOffset());
		fileOutput.write(idByteArray);
		fileOutput.seek(blockForSequence.getOffset());
		fileOutput.write(seqByteArray);
		MemBlock[] memHandles = new MemBlock[2];
		memHandles[0] = new MemBlock(seqID.length(), blockForID.getOffset());
		memHandles[1] = new MemBlock(sequence.length(), blockForSequence.getOffset());
		return memHandles;
	}

	// ----------------------------------------------------------
	/**
	 * Removes a sequence from the file by notating it as free space in the
	 * memory manager. It then returns the sequence string that was removed.
	 * @param idHandle the sequence ID memory handle
	 * @param seqHandle the sequence memory handle
	 * @param noFreeup a flag to not remove free space if sequence id is already in tree
	 * @return the removed sequence as a string
	 * @throws IOException
	 */
	public String removeSequence(MemBlock idHandle, MemBlock seqHandle, boolean noFreeup) throws IOException{

		byte[] removedSequence = new byte[seqHandle.getLength()];
		fileOutput.seek(seqHandle.getOffset());
		fileOutput.read(removedSequence);
		if(noFreeup){
			memMGR.adjustEndOfMemory(idHandle);
			memMGR.adjustEndOfMemory(seqHandle);
		}
		else{
			memMGR.freeUp(idHandle);
			memMGR.freeUp(seqHandle);
		}
		return bytesToString(removedSequence, seqHandle.getLength());
	}

	// ----------------------------------------------------------
	/**
	 * Converts a sequence string to a byte array where A is 00, C is 01, G is
	 * 10 and T is 11. Returns the byte array. Always works in increments of 4 bytes
	 * even if the sequence does not fill all of the last byte.
	 * @param sequence
	 * @param length
	 * @return a byte array of the encoded sequence
	 */
	public byte[] stringToBytes(String sequence, int length){
		byte[] seqInBytes = new byte[(int) Math.ceil(length/4.)];
		byte currByte = 0x0;
		StringReader strReader = new StringReader(sequence);
		char[] byteBuffer = new char[4];
		int numChars = 0;
		int totalBytes = 0;

		try {
			while((numChars = strReader.read(byteBuffer)) != -1){
				for(int i=0; i<numChars; i++){
					switch(byteBuffer[i]){
						case 'A':
							currByte = 0x0;
							break;
						case 'C':
							currByte = 0x1;
							break;
						case 'G':
							currByte = 0x2;
							break;
						case 'T':
							currByte = 0x3;
							break;
					}
					seqInBytes[totalBytes] = (byte) ((seqInBytes[totalBytes] << 2) | currByte);
				}
				totalBytes++;
			}
			for(int i=0; i<length%4;i++){
				seqInBytes[totalBytes-1] = (byte) (seqInBytes[totalBytes-1] << 2);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return seqInBytes;
	}

	// ----------------------------------------------------------
	/**
	 * Converts a byte array encoded sequence into a string of the sequence. And
	 * returns that string.
	 * @param seqInBytes
	 * @param length
	 * @return the sequence string
	 */
	public String bytesToString(byte[] seqInBytes, int length){
		String sequence = "";
		int numCharsRead = 0;
		int byteIndex = 0;
		byte twoBits;
		while(numCharsRead < length){
			for(int j=0; j < 4; j++){
				twoBits = (byte) (seqInBytes[byteIndex] & 0xC0);
				seqInBytes[byteIndex] = (byte) (seqInBytes[byteIndex] << 0x02);
				switch(twoBits){
				case 0x00:
					sequence += "A";
					numCharsRead++;
					break;
				case 0x40:
					sequence += "C";
					numCharsRead++;
					break;
				case (byte) 0x80:
					sequence += "G";
					numCharsRead++;
					break;
				case (byte) 0xC0:
					sequence += "T";
					numCharsRead++;
					break;
				}
				if(numCharsRead >= length )
					break;
			}
			byteIndex++;
		}
		return sequence;
	}

	// ----------------------------------------------------------
	/**
	 * Prints a listing of the free sapce.
	 */
	public void printFreeSpace(){
		memMGR.printFreeSpace();
	}

	// ----------------------------------------------------------
	/**
	 * Returns the memory manager being used by the file manager. This is to
	 * free up blocks of memory in the leaf nodes.
	 * @return the memory manager
	 */
	public MemoryManager getMemoryManager(){
		return memMGR;
	}
}
